This adds support for systemd soft reboots.
Closes: https://github.com/ostreedev/ostree/issues/3242
Signed-off-by: Colin Walters <walters@verbum.org>
Co-authored-by: Joseph Marrero Corchado <jmarrero@redhat.com>
Co-authored-by: Mary Strodl <ipadlover8322@gmail.com>
Signed-off-by: Colin Walters <walters@verbum.org>
src/libostree/ostree-sysroot-cleanup.c \
src/libostree/ostree-sysroot-deploy.c \
src/libostree/ostree-sysroot-upgrader.c \
+ src/libostree/ostree-soft-reboot.c \
src/libostree/ostree-impl-system-generator.c \
src/libostree/ostree-bootconfig-parser.c \
src/libostree/ostree-deployment.c \
symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym
# Uncomment this include when adding new development symbols.
-#if BUILDOPT_IS_DEVEL_BUILD
-#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
-#endif
+if BUILDOPT_IS_DEVEL_BUILD
+symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
+endif
# http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
wl_versionscript_arg = -Wl,--version-script=
ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1 \
ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1 \
ostree-admin-pin.1 ostree-admin-post-copy.1 ostree-admin-set-default.1 \
+ostree-admin-prepare-soft-reboot.1 \
ostree-admin-lock-finalization.1 \
ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 \
ostree-commit.1 ostree-create-usb.1 ostree-export.1 \
src/ostree/ot-admin-builtin-switch.c \
src/ostree/ot-admin-builtin-pin.c \
src/ostree/ot-admin-builtin-post-copy.c \
+ src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c \
+ src/ostree/ot-admin-builtin-prepare-soft-reboot.c \
src/ostree/ot-admin-builtin-upgrade.c \
src/ostree/ot-admin-builtin-unlock.c \
src/ostree/ot-admin-builtin-state-overlay.c \
ostree_deployment_is_pinned
ostree_deployment_is_staged
ostree_deployment_is_finalization_locked
+ostree_deployment_is_soft_reboot_target
ostree_deployment_set_index
ostree_deployment_set_bootserial
ostree_deployment_set_bootconfig
ostree_sysroot_deployment_set_mutable
ostree_sysroot_deployment_unlock
ostree_sysroot_deployment_set_pinned
+ostree_sysroot_deployment_can_soft_reboot
+ostree_sysroot_deployment_prepare_next_root
ostree_sysroot_write_deployments
ostree_sysroot_write_deployments_with_options
ostree_sysroot_write_origin_file
--- /dev/null
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<!--
+SPDX-License-Identifier: LGPL-2.0+
+-->
+
+<refentry id="ostree">
+
+ <refentryinfo>
+ <title>ostree admin prepare-soft-reboot</title>
+ <productname>OSTree</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>ostree admin prepare-soft-reboot</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>ostree-admin-prepare-soft-reboot</refname>
+ <refpurpose>Prepare the target deployment (via index) for soft reboot</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>ostree admin prepare-soft-reboot</command> <arg choice="req">INDEX</arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ Prepare the deployment at INDEX for a systemd soft reboot. INDEX must be in range and not reference the currently booted deployment.
+ It is recommended to immediately follow this with an involcation of <command>systemctl soft-reboot</command>.
+ </para>
+
+ <para>
+ It is not supported to soft reboot into a deployment with a different kernel than the booted one.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para><simplelist type="inline">
+ <member><citerefentry><refentrytitle>systemd-soft-reboot.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+ </simplelist></para>
+ </refsect1>
+</refentry>
# marked as a triggering condition in case in the future we want
# to do something else.
ConditionPathExists=|/boot/ostree/finalize-failure.stamp
+# Also run when soft-reboot cleanup is needed
+ConditionPathExists=|/run/ostree/nextroot-booted
# We start early
DefaultDependencies=no
After=sysinit.target
someostree_symbol_deleteme;
} LIBOSTREE_$YEAR.$LASTSTABLE;
*/
+
+LIBOSTREE_2025.3 {
+global:
+ ostree_deployment_is_soft_reboot_target;
+ ostree_sysroot_deployment_can_soft_reboot;
+ ostree_sysroot_deployment_prepare_next_root;
+} LIBOSTREE_2025.2;
_ostree_repo_static_delta_dump, _ostree_repo_static_delta_query_exists,
_ostree_repo_static_delta_delete, _ostree_repo_verify_bindings,
_ostree_sysroot_finalize_staged, _ostree_sysroot_boot_complete,
+ _ostree_prepare_soft_reboot,
};
return &table;
GError **error);
gboolean (*ostree_boot_complete) (OstreeSysroot *sysroot, GCancellable *cancellable,
GError **error);
+ gboolean (*ostree_prepare_soft_reboot) (GError **error);
} OstreeCmdPrivateVTable;
/* Note this not really "public", we just export the symbol, but not the header */
OstreeDeploymentUnlockedState unlocked;
gboolean staged;
gboolean finalization_locked;
+ gboolean soft_reboot_target;
char **overlay_initrds;
char *overlay_initrds_id;
};
{
return self->finalization_locked;
}
+
+/**
+ * ostree_deployment_is_soft_reboot_target:
+ * @self: Deployment
+ *
+ * Returns: `TRUE` if deployment is set for a soft reboot.
+ * Since: TODO
+ */
+gboolean
+ostree_deployment_is_soft_reboot_target (OstreeDeployment *self)
+{
+ return self->soft_reboot_target;
+}
_OSTREE_PUBLIC
gboolean ostree_deployment_is_finalization_locked (OstreeDeployment *self);
_OSTREE_PUBLIC
+gboolean ostree_deployment_is_soft_reboot_target (OstreeDeployment *self);
+_OSTREE_PUBLIC
gboolean ostree_deployment_is_pinned (OstreeDeployment *self);
_OSTREE_PUBLIC
--- /dev/null
+/* -*- c-file-style: "gnu" -*-
+ * Soft reboot for ostree. This code was originally derived from ostree-prepare-root.c,
+ * but is now significantly cut down to target specifically soft rebooting.
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ */
+
+#include "config.h"
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libglnx.h>
+#include <linux/magic.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <ostree-core.h>
+#include <ostree-sysroot-private.h>
+
+#include "ostree-mount-util.h"
+#include "ot-keyfile-utils.h"
+#include "otcore.h"
+
+/* This key configures the / mount in the deployment root */
+#define ROOT_KEY "root"
+#define ETC_KEY "etc"
+#define TRANSIENT_KEY "transient"
+
+gboolean
+_ostree_prepare_soft_reboot (GError **error)
+{
+ const char *sysroot_path = "/sysroot";
+ const char *target_deployment = ".";
+
+ g_autoptr (GKeyFile) config = otcore_load_config (AT_FDCWD, PREPARE_ROOT_CONFIG_PATH, error);
+ if (!config)
+ return FALSE;
+
+ gboolean root_transient = FALSE;
+ if (!ot_keyfile_get_boolean_with_default (config, ROOT_KEY, TRANSIENT_KEY, FALSE, &root_transient,
+ error))
+ return FALSE;
+
+ g_autofree char *kernel_cmdline = read_proc_cmdline ();
+ g_autoptr (ComposefsConfig) composefs_config
+ = otcore_load_composefs_config (kernel_cmdline, config, TRUE, error);
+ if (!composefs_config)
+ return FALSE;
+
+ if (composefs_config->enabled != OT_TRISTATE_YES)
+ return glnx_throw (error, "soft reboot not supported without composefs");
+
+ GVariantBuilder metadata_builder;
+ g_variant_builder_init (&metadata_builder, G_VARIANT_TYPE ("a{sv}"));
+
+ if (!glnx_shutil_mkdir_p_at (AT_FDCWD, OTCORE_RUN_NEXTROOT, 0755, NULL, error))
+ return FALSE;
+
+ // Tracks if we did successfully enable it at runtime
+ bool using_composefs = false;
+ if (!otcore_mount_rootfs (composefs_config, &metadata_builder, root_transient, sysroot_path,
+ target_deployment, OTCORE_RUN_NEXTROOT, &using_composefs, error))
+ return glnx_prefix_error (error, "failed to mount composefs");
+
+ if (!using_composefs)
+ return glnx_throw (error, "failed to mount with composefs");
+
+ if (!otcore_mount_etc (config, &metadata_builder, OTCORE_RUN_NEXTROOT, error))
+ return FALSE;
+
+ // Note we should have inherited the readonly sysroot
+ g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL);
+ if (mount (sysroot_path, target_sysroot, NULL, MS_BIND | MS_SILENT, NULL) < 0)
+ return glnx_throw_errno_prefix (error, "failed to bind mount sysroot");
+
+ /* This can be used by other things to signal ostree is in use */
+ {
+ g_autoptr (GVariant) metadata = g_variant_ref_sink (g_variant_builder_end (&metadata_builder));
+ const guint8 *buf = g_variant_get_data (metadata) ?: (guint8 *)"";
+ if (!glnx_file_replace_contents_at (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, buf,
+ g_variant_get_size (metadata), 0, NULL, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
if (!ot_openat_ignore_enoent (self->boot_fd, _OSTREE_FINALIZE_STAGED_FAILURE_PATH, &failure_fd,
error))
return FALSE;
- // If we didn't find a failure log, then there's nothing to do right now.
- // (Actually this unit shouldn't even be invoked, but we may do more in the future)
+ // If we didn't find a failure log, check for soft-reboot completion tasks
if (failure_fd == -1)
- return TRUE;
+ {
+ // Check if we just completed a soft-reboot and need to update /run/ostree-booted
+ // We're completing a soft-reboot, simply move the nextroot-booted file to ostree-booted
+ if (rename (OTCORE_RUN_NEXTROOT_BOOTED, OTCORE_RUN_BOOTED) < 0)
+ {
+ if (errno != ENOENT)
+ return glnx_throw_errno_prefix (error, "Failed to rename %s to %s",
+ OTCORE_RUN_NEXTROOT_BOOTED, OTCORE_RUN_BOOTED);
+ g_debug ("Updated /run/ostree-booted for soft-reboot completion");
+ }
+ return TRUE;
+ }
g_autofree char *failure_data = glnx_fd_readall_utf8 (failure_fd, NULL, cancellable, error);
if (failure_data == NULL)
return glnx_prefix_error (error, "Reading from %s", _OSTREE_FINALIZE_STAGED_FAILURE_PATH);
return TRUE;
}
+struct PrepareRootChildSetupContext
+{
+ const char *deployment_path;
+ int rootns_fd;
+};
+
+static inline void
+prepare_root_child_setup (gpointer data)
+{
+ struct PrepareRootChildSetupContext *ctx = data;
+ // Enter the root namespace first to escape the overlayfs context
+ int rc = setns (ctx->rootns_fd, CLONE_NEWNS);
+ if (rc < 0)
+ err (1, "setns");
+ // Then change to the deployment directory in the root namespace
+ rc = chdir (ctx->deployment_path);
+ if (rc < 0)
+ err (1, "chdir");
+}
+
+/**
+ * ostree_sysroot_deployment_can_soft_reboot:
+ * @self: The #OstreeSysroot object.
+ * @deployment: The #OstreeDeployment to check for soft-reboot compatibility.
+ *
+ * Checks if the given deployment can be soft-rebooted to from the currently
+ * booted deployment. A soft-reboot is generally only possible if both the
+ * currently booted deployment and the target `deployment` use the same kernel
+ * (i.e., have the same boot checksum).
+ *
+ * Returns: %TRUE if a soft-reboot is possible to the target deployment, %FALSE otherwise.
+ * Since: TODO
+ */
+gboolean
+ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment *deployment)
+{
+ OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (self);
+ if (booted_deployment != NULL)
+ {
+ const char *booted_bootcsum = ostree_deployment_get_bootcsum (booted_deployment);
+ const char *target_bootcsum = ostree_deployment_get_bootcsum (deployment);
+ return g_str_equal (booted_bootcsum, target_bootcsum);
+ }
+ return false;
+}
+
+/**
+ * ostree_sysroot_deployment_prepare_next_root
+ * @self: Sysroot
+ * @deployment: Deployment to prepare /run/nextroot
+ * @allow_kernel_skew: Continue even if there is a kernel mismatch
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Prepare the specified deployment for a systemd soft-reboot by creating a new
+ * root with it at `/run/nextroot`.
+ *
+ * Since: TODO
+ */
+gboolean
+ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployment *deployment,
+ gboolean allow_kernel_skew, GCancellable *cancellable,
+ GError **error)
+{
+ GLNX_AUTO_PREFIX_ERROR ("Preparing /run/nextroot for a soft-reboot", error);
+
+ if (!ostree_sysroot_deployment_can_soft_reboot (self, deployment) && !allow_kernel_skew)
+ {
+ return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel");
+ }
+
+ // For targeting a staged deployment, we finalize now to ensure that we have /etc
+ if (ostree_deployment_is_staged (deployment))
+ {
+ if (!_ostree_sysroot_finalize_staged (self, NULL, error))
+ return FALSE;
+ }
+
+ g_autofree char *deployment_relpath = ostree_sysroot_get_deployment_dirpath (self, deployment);
+ g_autofree char *deployment_fullpath = g_build_filename ("/sysroot", deployment_relpath, NULL);
+ gint estatus;
+
+ const char *argv[] = { "ostree", "admin", "impl-prepare-soft-reboot", NULL };
+
+ glnx_autofd int rootns_fd = -1;
+ if (!glnx_openat_rdonly (AT_FDCWD, "/proc/1/ns/mnt", TRUE, &rootns_fd, error))
+ return FALSE;
+
+ struct PrepareRootChildSetupContext ctx = {
+ .deployment_path = deployment_fullpath,
+ .rootns_fd = rootns_fd,
+ };
+
+ if (!g_spawn_sync (NULL, (char **)argv, NULL, G_SPAWN_SEARCH_PATH, prepare_root_child_setup, &ctx,
+ NULL, NULL, &estatus, error))
+ return FALSE;
+
+ if (!g_spawn_check_exit_status (estatus, error))
+ {
+ int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL;
+ // If we failed to initialize the soft reboot, ensure that we've unwound any mounts
+ const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL };
+ // To aid debugging allow skipping cleanup on failure
+ if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP"))
+ g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL);
+ return FALSE;
+ }
+
+ ot_journal_print (LOG_INFO, "Set up soft reboot at /run/nextroot");
+
+ return TRUE;
+}
+
/**
* ostree_sysroot_deployment_kexec_load
* @self: Sysroot
/* The device/inode for / and /etc, used to detect booted deployment */
dev_t root_device;
ino_t root_inode;
+ /* The device inode for a queued soft reboot deployment */
+ gboolean have_nextroot;
+ dev_t nextroot_device;
+ ino_t nextroot_inode;
// The parsed data from /run/ostree
GVariantDict *run_ostree_metadata;
gboolean _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable,
GError **error);
+gboolean _ostree_prepare_soft_reboot (GError **error);
+
OstreeDeployment *_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v, GError **error);
char *_ostree_sysroot_get_deployment_backing_relpath (OstreeDeployment *deployment);
if (!glnx_opendirat (self->sysroot_fd, relative_boot_link, TRUE, &deployment_dfd, error))
return FALSE;
+ struct stat stbuf;
+ if (!glnx_fstat (deployment_dfd, &stbuf, error))
+ return FALSE;
+
/* See if this is the booted deployment */
const gboolean looking_for_booted_deployment
= (self->root_is_ostree_booted && !self->booted_deployment);
gboolean is_booted_deployment = FALSE;
if (looking_for_booted_deployment)
{
- struct stat stbuf;
- if (!glnx_fstat (deployment_dfd, &stbuf, error))
- return FALSE;
-
/* ostree-prepare-root records the (device, inode) pair of the underlying real deployment
* directory (before we might have mounted a composefs or overlayfs on top).
*
is_booted_deployment
= stbuf.st_dev == expected_root_dev && stbuf.st_ino == expected_root_inode;
}
+ gboolean is_soft_reboot_target
+ = self->have_nextroot
+ && (stbuf.st_dev == self->nextroot_device && stbuf.st_ino == self->nextroot_inode);
g_autoptr (OstreeDeployment) ret_deployment
= ostree_deployment_new (-1, osname, treecsum, deployserial, bootcsum, treebootserial);
ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
g_autofree char *unlocked_transient_path = _ostree_sysroot_get_runstate_path (
ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_TRANSIENT);
- struct stat stbuf;
if (lstat (unlocked_development_path, &stbuf) == 0)
ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
else if (lstat (unlocked_transient_path, &stbuf) == 0)
}
/* TODO: warn on unknown unlock types? */
}
+ ret_deployment->soft_reboot_target = is_soft_reboot_target;
g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked);
if (!self->root_is_ostree_booted)
return TRUE; /* Note early return */
- g_assert (self->booted_deployment);
+ /* In normal cases, we should have a booted deployment. However, during
+ * soft-reboot scenarios, the current deployment may not correspond to
+ * any bootloader entry, so booted_deployment could be NULL. */
+ if (!self->booted_deployment)
+ {
+ /* Check if we're in a soft-reboot scenario */
+ if (!(g_file_test ("/run/nextroot", G_FILE_TEST_IS_DIR)
+ && g_file_test ("/run/nextroot/sysroot", G_FILE_TEST_IS_DIR)))
+ {
+ g_assert (self->booted_deployment);
+ }
+ }
g_clear_object (&self->staged_deployment);
g_clear_pointer (&self->staged_deployment_data, g_variant_unref);
return TRUE;
}
+/* Reload state from /run/ostree/nextroot-booted */
+static gboolean
+_ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error)
+{
+ GLNX_AUTO_PREFIX_ERROR ("Loading nextroot", error);
+ // Reset state
+ self->have_nextroot = FALSE;
+
+ glnx_autofd int fd = -1;
+ if (!ot_openat_ignore_enoent (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, &fd, error))
+ return FALSE;
+ // If there's no such file, we're done
+ if (fd == -1)
+ return TRUE;
+
+ // Parse the GVariant metadata from this; search for OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO
+ // to find similar code.
+ g_autoptr (GVariant) metadata = NULL;
+ if (!ot_variant_read_fd (fd, 0, G_VARIANT_TYPE_VARDICT, TRUE, &metadata, error))
+ return glnx_prefix_error (error, "failed to read %s", OTCORE_RUN_NEXTROOT_BOOTED);
+
+ // Get the backing device/inode from metadata
+ guint64 backing_dev, backing_ino;
+ g_autoptr (GVariant) backing_devino = g_variant_lookup_value (
+ metadata, OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO, G_VARIANT_TYPE ("(tt)"));
+ if (!backing_devino)
+ return glnx_throw (error, "Missing %s key in %s", OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO,
+ OTCORE_RUN_NEXTROOT_BOOTED);
+
+ // Load the device/inode, and we're done
+ g_variant_get (backing_devino, "(tt)", &backing_dev, &backing_ino);
+ self->have_nextroot = TRUE;
+ self->nextroot_device = (dev_t)backing_dev;
+ self->nextroot_inode = (ino_t)backing_ino;
+
+ return TRUE;
+}
+
/* Loads the current bootversion, subbootversion, and deployments, starting from the
* bootloader configs which are the source of truth.
*/
error))
return FALSE;
+ if (!_ostree_sysroot_reload_soft_reboot (self, error))
+ return FALSE;
+
g_autoptr (GPtrArray) boot_loader_configs = NULL;
if (!_ostree_sysroot_read_boot_loader_configs (self, bootversion, &boot_loader_configs,
cancellable, error))
OstreeSysrootSimpleWriteDeploymentFlags flags,
GCancellable *cancellable, GError **error);
+_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self,
+ OstreeDeployment *deployment);
+
+_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self,
+ OstreeDeployment *deployment,
+ gboolean allow_kernel_skew,
+ GCancellable *cancellable,
+ GError **error);
+
_OSTREE_PUBLIC
gboolean ostree_sysroot_deployment_kexec_load (OstreeSysroot *self, OstreeDeployment *deployment,
GCancellable *cancellable, GError **error);
#define OTCORE_PREPARE_ROOT_KEYPATH_KEY "keypath"
#define OTCORE_PREPARE_ROOT_TRANSIENT_KEY "transient"
+// For use with systemd soft reboots
+#define OTCORE_RUN_NEXTROOT "/run/nextroot"
+
// The file written in the initramfs which contains an a{sv} of metadata
// from ostree-prepare-root.
#define OTCORE_RUN_BOOTED "/run/ostree-booted"
+// Written by ostree-soft-reboot.c with metadata about /run/nextroot
+#define OTCORE_RUN_NEXTROOT_BOOTED "/run/ostree/nextroot-booted"
// This key will be present if composefs was successfully used.
#define OTCORE_RUN_BOOTED_KEY_COMPOSEFS "composefs"
// True if fsverity was required for composefs.
--- /dev/null
+/*
+ * SPDX-License-Identifier: LGPL-2.0+
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include "ostree.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "otutil.h"
+
+#include "ostree-cmd-private.h"
+
+gboolean
+ot_admin_builtin_impl_prepare_soft_reboot (int argc, char **argv,
+ OstreeCommandInvocation *invocation,
+ GCancellable *cancellable, GError **error)
+{
+ if (!ostree_cmd__private__ ()->ostree_prepare_soft_reboot (error))
+ return FALSE;
+
+ return TRUE;
+}
--- /dev/null
+/*
+ * Copyright (C) 2025 Colin Walters <walters@verbum.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include "ostree.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "otutil.h"
+
+static GOptionEntry options[] = { { NULL } };
+
+gboolean
+ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvocation *invocation,
+ GCancellable *cancellable, GError **error)
+{
+ g_autoptr (GOptionContext) context = g_option_context_new ("INDEX");
+
+ g_autoptr (OstreeSysroot) sysroot = NULL;
+ if (!ostree_admin_option_context_parse (context, options, &argc, &argv,
+ OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER, invocation, &sysroot,
+ cancellable, error))
+ return FALSE;
+
+ if (argc < 2)
+ {
+ ot_util_usage_error (context, "INDEX must be specified", error);
+ return FALSE;
+ }
+
+ const char *deploy_index_str = argv[1];
+ guint deploy_index;
+ {
+ char *endptr = NULL;
+ errno = 0;
+ deploy_index = (guint)g_ascii_strtoull (deploy_index_str, &endptr, 10);
+ if (*endptr != '\0')
+ return glnx_throw (error, "Invalid index: %s", deploy_index_str);
+ }
+
+ g_autoptr (OstreeDeployment) target_deployment
+ = ot_admin_get_indexed_deployment (sysroot, deploy_index, error);
+ if (!target_deployment)
+ return FALSE;
+
+ if (target_deployment == ostree_sysroot_get_booted_deployment (sysroot))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+ "Cannot prepare for soft-reboot currently booted deployment %i", deploy_index);
+ return FALSE;
+ }
+
+ if (!ostree_sysroot_deployment_prepare_next_root (sysroot, target_deployment, FALSE, cancellable,
+ error))
+ return FALSE;
+
+ return TRUE;
+}
"Output \"default\" if booted into the default deployment, otherwise \"not-default\"",
NULL },
{ NULL } };
+
static gboolean
deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment,
gboolean is_booted, gboolean is_pending, gboolean is_rollback,
g_autofree char *origin_refspec
= origin ? g_key_file_get_string (origin, "origin", "refspec", NULL) : NULL;
- const char *deployment_status = "";
+ g_autoptr (GString) deployment_status = g_string_new ("");
+
if (ostree_deployment_is_finalization_locked (deployment))
- deployment_status = " (finalization locked)";
+ g_string_append (deployment_status, " (finalization locked)");
else if (ostree_deployment_is_staged (deployment))
- deployment_status = " (staged)";
+ g_string_append (deployment_status, " (staged)");
else if (is_pending)
- deployment_status = " (pending)";
+ g_string_append (deployment_status, " (pending)");
else if (is_rollback)
- deployment_status = " (rollback)";
- g_print ("%c %s %s.%d%s\n", is_booted ? '*' : ' ', ostree_deployment_get_osname (deployment),
+ g_string_append (deployment_status, " (rollback)");
+
+ if (ostree_deployment_is_soft_reboot_target (deployment))
+ g_string_append (deployment_status, " (soft-reboot)");
+
+ char deployment_marker = is_booted ? '*' : ' ';
+ g_print ("%c %s %s.%d%s\n", deployment_marker, ostree_deployment_get_osname (deployment),
ostree_deployment_get_csum (deployment), ostree_deployment_get_deployserial (deployment),
- deployment_status);
+ deployment_status->str);
if (version)
g_print (" Version: %s\n", version);
BUILTINPROTO (pin);
BUILTINPROTO (finalize_staged);
BUILTINPROTO (boot_complete);
+BUILTINPROTO (prepare_soft_reboot);
+BUILTINPROTO (impl_prepare_soft_reboot);
BUILTINPROTO (unlock);
BUILTINPROTO (status);
BUILTINPROTO (set_origin);
"Change the finalization locking state of the staged deployment" },
{ "boot-complete", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
ot_admin_builtin_boot_complete, "Internal command to run at boot after an update was applied" },
+ { "impl-prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
+ ot_admin_builtin_impl_prepare_soft_reboot, "Internal command to prepare soft reboot" },
{ "state-overlay", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
ot_admin_builtin_state_overlay, "Internal command to assemble a state overlay" },
{ "init-fs", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_init_fs,
"rollback strings" },
{ "post-copy", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_post_copy,
"Update the repo and deployments as needed after a copy" },
+ { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot,
+ "Prepare deployment for soft-reboot" },
{ "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_set_origin,
"Set Origin and create a new origin file" },
{ "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" },
--- /dev/null
+#!/bin/bash
+set -xeuo pipefail
+
+. ${KOLA_EXT_DATA}/libinsttest.sh
+
+require_writable_sysroot
+prepare_tmpdir
+
+case "${AUTOPKGTEST_REBOOT_MARK:-}" in
+ "")
+ # xref https://github.com/coreos/coreos-assembler/pull/2814
+ systemctl mask --now zincati
+
+ assert_streq $(systemctl show -P SoftRebootsCount) 0
+
+ # Create a synthetic commit for upgrade
+ cd /ostree/repo/tmp
+ ostree checkout -H ${host_commit} t
+ unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/etc/new-file-for-soft-reboot usr/share/test-file-for-soft-reboot'
+ ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test -I --consume t
+ newcommit=$(ostree rev-parse soft-reboot-test)
+ # Deploy the new commit normally first
+ ostree admin deploy --stage soft-reboot-test
+
+ # Test prepare-soft-reboot command
+ echo "Testing prepare-soft-reboot..."
+ ostree admin prepare-soft-reboot 0
+
+ ostree admin status > status.txt
+ assert_file_has_content_literal status.txt '(pending) (soft-reboot)'
+
+ test -f /run/ostree/nextroot-booted
+
+ /tmp/autopkgtest-soft-reboot "2"
+ ;;
+ "2")
+ # After soft reboot, verify we're running the new deployment
+ echo "Verifying post-soft-reboot state..."
+ assert_streq $(systemctl show -P SoftRebootsCount) 1
+
+ expected_commit=$(ostree rev-parse soft-reboot-test)
+
+ if [ "${host_commit}" != "${expected_commit}" ]; then
+ echo "ERROR: Expected commit ${host_commit}, but got ${current_commit}"
+ exit 1
+ fi
+
+ test -f /etc/new-file-for-soft-reboot
+ test -f /usr/share/test-file-for-soft-reboot
+
+ # Verify that soft-reboot-pending file is cleaned up
+ test '!' -f /run/ostree/nextroot-booted
+
+ echo "Soft reboot test completed successfully!"
+ ;;
+ *)
+ fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}"
+ ;;
+esac